use std::path::{Path, PathBuf};

use crate::command::Command;
use crate::env::env_var;

/// Construct a new `llvm-readobj` invocation with the `GNU` output style.
/// This assumes that `llvm-readobj` is available at `$LLVM_BIN_DIR/llvm-readobj`.
#[track_caller]
pub fn llvm_readobj() -> LlvmReadobj {
    LlvmReadobj::new()
}

/// Construct a new `llvm-profdata` invocation. This assumes that `llvm-profdata` is available
/// at `$LLVM_BIN_DIR/llvm-profdata`.
#[track_caller]
pub fn llvm_profdata() -> LlvmProfdata {
    LlvmProfdata::new()
}

/// Construct a new `llvm-filecheck` invocation. This assumes that `llvm-filecheck` is available
/// at `$LLVM_FILECHECK`.
#[track_caller]
pub fn llvm_filecheck() -> LlvmFilecheck {
    LlvmFilecheck::new()
}

/// Construct a new `llvm-objdump` invocation. This assumes that `llvm-objdump` is available
/// at `$LLVM_BIN_DIR/llvm-objdump`.
pub fn llvm_objdump() -> LlvmObjdump {
    LlvmObjdump::new()
}

/// Construct a new `llvm-ar` invocation. This assumes that `llvm-ar` is available
/// at `$LLVM_BIN_DIR/llvm-ar`.
pub fn llvm_ar() -> LlvmAr {
    LlvmAr::new()
}

/// A `llvm-readobj` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmReadobj {
    cmd: Command,
}

/// A `llvm-profdata` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmProfdata {
    cmd: Command,
}

/// A `llvm-filecheck` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmFilecheck {
    cmd: Command,
}

/// A `llvm-objdump` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmObjdump {
    cmd: Command,
}

/// A `llvm-ar` invocation builder.
#[derive(Debug)]
#[must_use]
pub struct LlvmAr {
    cmd: Command,
}

crate::macros::impl_common_helpers!(LlvmReadobj);
crate::macros::impl_common_helpers!(LlvmProfdata);
crate::macros::impl_common_helpers!(LlvmFilecheck);
crate::macros::impl_common_helpers!(LlvmObjdump);
crate::macros::impl_common_helpers!(LlvmAr);

/// Generate the path to the bin directory of LLVM.
#[must_use]
pub fn llvm_bin_dir() -> PathBuf {
    let llvm_bin_dir = env_var("LLVM_BIN_DIR");
    PathBuf::from(llvm_bin_dir)
}

impl LlvmReadobj {
    /// Construct a new `llvm-readobj` invocation with the `GNU` output style.
    /// This assumes that `llvm-readobj` is available at `$LLVM_BIN_DIR/llvm-readobj`.
    #[track_caller]
    pub fn new() -> Self {
        let llvm_readobj = llvm_bin_dir().join("llvm-readobj");
        let cmd = Command::new(llvm_readobj);
        let mut readobj = Self { cmd };
        readobj.elf_output_style("GNU");
        readobj
    }

    /// Specify the format of the ELF information.
    ///
    /// Valid options are `LLVM` (default), `GNU`, and `JSON`.
    pub fn elf_output_style(&mut self, style: &str) -> &mut Self {
        self.cmd.arg("--elf-output-style");
        self.cmd.arg(style);
        self
    }

    /// Provide an input file.
    pub fn input<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cmd.arg(path.as_ref());
        self
    }

    /// Pass `--file-header` to display file headers.
    pub fn file_header(&mut self) -> &mut Self {
        self.cmd.arg("--file-header");
        self
    }

    /// Pass `--program-headers` to display program headers.
    pub fn program_headers(&mut self) -> &mut Self {
        self.cmd.arg("--program-headers");
        self
    }

    /// Pass `--symbols` to display the symbol.
    pub fn symbols(&mut self) -> &mut Self {
        self.cmd.arg("--symbols");
        self
    }

    /// Pass `--dynamic-table` to display the dynamic symbol table.
    pub fn dynamic_table(&mut self) -> &mut Self {
        self.cmd.arg("--dynamic-table");
        self
    }

    /// Specify the section to display.
    pub fn section(&mut self, section: &str) -> &mut Self {
        self.cmd.arg("--string-dump");
        self.cmd.arg(section);
        self
    }
}

impl LlvmProfdata {
    /// Construct a new `llvm-profdata` invocation. This assumes that `llvm-profdata` is available
    /// at `$LLVM_BIN_DIR/llvm-profdata`.
    #[track_caller]
    pub fn new() -> Self {
        let llvm_profdata = llvm_bin_dir().join("llvm-profdata");
        let cmd = Command::new(llvm_profdata);
        Self { cmd }
    }

    /// Provide an input file.
    pub fn input<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cmd.arg(path.as_ref());
        self
    }

    /// Specify the output file path.
    pub fn output<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cmd.arg("-o");
        self.cmd.arg(path.as_ref());
        self
    }

    /// Take several profile data files generated by PGO instrumentation and merge them
    /// together into a single indexed profile data file.
    pub fn merge(&mut self) -> &mut Self {
        self.cmd.arg("merge");
        self
    }
}

impl LlvmFilecheck {
    /// Construct a new `llvm-filecheck` invocation. This assumes that `llvm-filecheck` is available
    /// at `$LLVM_FILECHECK`.
    #[track_caller]
    pub fn new() -> Self {
        let llvm_filecheck = env_var("LLVM_FILECHECK");
        let cmd = Command::new(llvm_filecheck);
        Self { cmd }
    }

    /// Pipe a read file into standard input containing patterns that will be matched against the .patterns(path) call.
    pub fn stdin<I: AsRef<[u8]>>(&mut self, input: I) -> &mut Self {
        self.cmd.stdin(input);
        self
    }

    /// Provide the patterns that need to be matched.
    pub fn patterns<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cmd.arg(path.as_ref());
        self
    }

    /// `--input-file` option.
    pub fn input_file<P: AsRef<Path>>(&mut self, input_file: P) -> &mut Self {
        self.cmd.arg("--input-file");
        self.cmd.arg(input_file.as_ref());
        self
    }
}

impl LlvmObjdump {
    /// Construct a new `llvm-objdump` invocation. This assumes that `llvm-objdump` is available
    /// at `$LLVM_BIN_DIR/llvm-objdump`.
    pub fn new() -> Self {
        let llvm_objdump = llvm_bin_dir().join("llvm-objdump");
        let cmd = Command::new(llvm_objdump);
        Self { cmd }
    }

    /// Provide an input file.
    pub fn input<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
        self.cmd.arg(path.as_ref());
        self
    }
}

impl LlvmAr {
    /// Construct a new `llvm-ar` invocation. This assumes that `llvm-ar` is available
    /// at `$LLVM_BIN_DIR/llvm-ar`.
    pub fn new() -> Self {
        let llvm_ar = llvm_bin_dir().join("llvm-ar");
        let cmd = Command::new(llvm_ar);
        Self { cmd }
    }

    pub fn obj_to_ar(&mut self) -> &mut Self {
        self.cmd.arg("rcus");
        self
    }

    /// Provide an output, then an input file. Bundled in one function, as llvm-ar has
    /// no "--output"-style flag.
    pub fn output_input(&mut self, out: impl AsRef<Path>, input: impl AsRef<Path>) -> &mut Self {
        self.cmd.arg(out.as_ref());
        self.cmd.arg(input.as_ref());
        self
    }
}
